// added limit function to keep squares in play // changed lock management to d3.map on __data__ // delegated data bind to web worker // data transfer via JSON serialisation // lock moved off __data__ onto ___locks // putting it on __data__ is not thread safe because transitions terminate // during the worker cycle $(function () { var rects, gridHeight = 500, gridWidth = 960, gridExtent, squaresExtent, cellSize, cellPitch, cellsColumns = 100, cellsRows = 50, cellsCount = cellsColumns * cellsRows, squares = [], inputs = d3.select("body").insert("div", '.svg-container') .attr("id", "metrics"), elapsedTime = outputs.ElapsedTime("#metrics", { border: 0, margin: 0, "box-sizing": "border-box", padding: "0 0 0 6px", background: "#2B303B", "color": "orange" }), hist = d3.ui.FpsMeter("#metrics", {display: "inline-block"}, { height: 10, width: 100, values: function(d){return 1/d}, domain: [0, 60] }), container = d3.select('.svg-container'), svg = container.append('svg') .attr('width', gridWidth) .attr('height', (gridHeight = gridHeight - metrics.clientHeight)) .style({ 'background-color': 'black', opacity: 1 }), createRandomRGB = function () { var red = Math.floor((Math.random() * 256)).toString(), green = Math.floor((Math.random() * 256)).toString(), blue = Math.floor((Math.random() * 256)).toString(), rgb = 'rgb(' + red + ',' + green + ',' + blue + ')'; return [red, green, blue]; }, createGrid = function (width, height) { var scaleHorizontal = d3.scale.ordinal() .domain(d3.range(cellsColumns)) .rangeBands([0, width], 1 / 15), rangeHorizontal = scaleHorizontal.range(), scaleVertical = d3.scale.ordinal() .domain(d3.range(cellsRows)) .rangeBands([0, height]), rangeVertical = scaleVertical.range(), squares = []; rangeHorizontal.forEach(function (dh, i) { rangeVertical.forEach(function (dv, j) { var indx; squares[indx = i + j * cellsColumns] = { x: dh, y: dv, c: createRandomRGB(), indx: indx } }) }); cellSize = scaleHorizontal.rangeBand(); cellPitch = { x: rangeHorizontal[1] - rangeHorizontal[0], y: rangeVertical[1] - rangeVertical[0] }; gridExtent = { x: scaleHorizontal.rangeExtent(), y: scaleVertical.rangeExtent() }; squaresExtent = { x: [gridExtent.x[0], gridExtent.x[1] - cellPitch.x], y: [gridExtent.y[0], gridExtent.y[1] - cellPitch.y] } rects = svg.selectAll("rect") .data(squares, function (d, i) { return d.indx }) .enter().append('rect') .attr('class', 'cell') .attr('width', cellSize) .attr('height', cellSize) .attr('x', function (d) { return d.x }) .attr('y', function (d) { return d.y }) .style('fill', function (d) { return "rgb(" + d.c.join(",") + ")"}); return squares; }, choseRandom = function (options) { options = options || [true, false]; var max = options.length; return options[Math.floor(Math.random() * (max))]; }, pickRandomCell = function (selection, group) { //cells is a group from a selection, i.e. the second dimension of the array //it may be filtered so use a global cell count as a basis for computing size var l = cellsCount - locked.count, r = Math.floor(Math.random() * l); return l ? d3.select(selection[group][r]).datum().indx : -1; }, locked = (function () { var lockedNodeCount = 0; function l(name, value) { // is a DOM element //can be called with name as an object to manage multiple locks if (typeof name === "string") { if (value) { if (!this.___locks) { this.___locks = d3.map(); lockedNodeCount++; } this.___locks.set(name, value); } else { this.___locks.remove(name); if (this.___locks.empty()) { delete this.___locks; lockedNodeCount--; } } } else { //name is an object, recurse multiple locks for (var p in name) locked(p, name[p]); } }; Object.defineProperty(l, "count", { get: function () { return lockedNodeCount; } }) return l; })(); function lock(lockClass) { // is the node locked.call(this, lockClass, true) } function unlock(lockClass) { // is the node locked.call(this, lockClass, false) } function permutateColours(cells, group, squares) { var samples = Math.min(10, Math.max(~~(squares.length / 5), 1)), s, ii = [], i, k = 0, c; while (samples--) { do i = pickRandomCell(cells, group); while (ii.indexOf(i) > -1 && k++ < 5 && i > -1); if (k < 10 && i > -1) { ii.push(i); s = squares[i]; squares.splice(i, 1, { x: s.x, y: s.y, c: createRandomRGB(), indx: s.indx }); } } } function permutatePositions(cells, group, squares) { var samples = Math.min(10, Math.max(~~(squares.length / 10), 1)), s, ss = [], d, m, p, k = 0; while (samples--) { do s = pickRandomCell(cells, group); while (ss.indexOf(s) > -1 && k++ < 5 && s > -1); if (k < 10 && s > -1) { ss.push(s); d = squares[s]; m = { x: d.x, y: d.y, c: d.c, indx: d.indx }; m[p = choseRandom(["x", "y"])] = limit(m[p] + choseRandom([-1, 1]) * cellPitch[p], squaresExtent[p]); squares.splice(s, 1, m); } } function limit (value, extent) { var min = extent[0], max = extent[1]; return Math.min(max, Math.max(value, min)) } } function getChanges(rects, squares) { //use a composite key function to use the exit selection as an attribute update selection //since its the exit selection, d3 does not bind the new data, this is done with the .each return rects .data(squares, function (d, i) { return d.indx + "_" + d.x + "_" + d.y + "_" + d.c.join("_"); }) .exit().each(function (d, i, j) { d3.select(this).datum(squares[i]) }) } function updateSquaresXY(changes) { changes .transition("strokex").duration(600) .attr("stroke", "white") .style("stroke-opacity", 0.6) .transition("x").duration(1500) .attr('x', function (d) { return d.x }) .each("start", function (d) { lock.call(this, "lockedX") }) .each("end", function (d) { unlock.call(this, "lockedX") }) .transition("strokex").duration(600) .style("stroke-opacity", 0) changes .transition("y").duration(1500) .attr('y', function (d) { return d.y }) .each("start", function (d) { lock.call(this, "lockedY") }) .each("end", function (d) { unlock.call(this, "lockedY") }) .transition("strokey").duration(600) .style("stroke-opacity", 0) } function updateSquaresX(changes) { changes .transition("strokex").duration(600) .filter(function () { return !this.___locks }) .attr("stroke", "white") .style("stroke-opacity", 0.6) .transition("x").duration(1500) .attr('x', function (d) { return d.x }) .each("start", function (d) { lock.call(this, "lockedX") }) .each("end", function (d) { unlock.call(this, "lockedX") }) .transition("strokex").duration(600) .style("stroke-opacity", 0) } function updateSquaresY(changes) { changes .transition("strokey").duration(600) .filter(function () { return !this.___locks }) .attr("stroke", "white") .style("stroke-opacity", 0.6) .transition("y").duration(1500) .attr('y', function (d) { return d.y }) .each("start", function (d) { lock.call(this, "lockedY") }) .each("end", function (d) { unlock.call(this, "lockedY") }) .transition("strokey").duration(600) .style("stroke-opacity", 0) } function updateSquaresFill(changes) { changes.style("opacity", 0.6).transition("flash").duration(250).style("opacity", 1) .transition("fill").duration(800) .style('fill', function (d, i) { return "rgb(" + d.c.join(",") + ")" }) .each("start", function (d) { lock.call(this, "lockedFill") }) .each("end", function (d) { unlock.call(this, "lockedFill") }); } squares = createGrid(gridWidth, gridHeight); var changes, exmpleKeyDescr = { base: squares[0], include: ["indx", "x", "y"] }, rebindX = RebindWorker(["indx", "x"], function x(changes) { updateSquaresX(changes); }), rebindY = RebindWorker(["indx", "y"], function y(changes) { updateSquaresY(changes); }), rebindFill = RebindWorker(["indx", "c"], function fill(changes) { updateSquaresFill(changes); }); $.when(rebindX.done, rebindY.done, rebindFill.done).done(function () { squares_tick(squares) }); function squares_tick(squares) { d3.timer(function t () { var dormantRects = rects.filter(function (d, i) { return !this.___locks }), _changes, rectsJSON = {data: null, serialised: true}; permutateColours(dormantRects,0, squares); rebindFill.postChanges(rects, squares); permutatePositions(dormantRects,0, squares); rebindX.postChanges(rects, squares); rebindY.postChanges(rects, squares); $.when(rebindX.done, rebindY.done, rebindFill.done).done(function () { squares_tick(squares) }); return true updateSquaresXY(_changes = getChanges(rects, squares)); }); } function RebindWorker(keyDescriptor, updateThen) { //dependency jquery Deferred var dataFrame = TransfSelection(), rebind = new Worker("updateSquares worker v2.js"); //custom methods rebind.changes = function (buffer) { var args; changes = dataFrame.selection(buffer); //the message serialisation process truncates trailing null array entries //re-establish these by adjusting the length of each group in the selection rects.forEach(function restoreLength(d, i) { changes[i].length = d.length }); //re-bind the d3 selection behaviour to the returned object Object.keys(d3.selection.prototype).forEach(function (p, i, o) { changes[p] = d3.selection.prototype[p] }); //put the new data on the changed nodes changes.each(function reData(d, i, j) { d3.select(rects[j][i]).datum(d); }); //put the dom elements on the newly created changes selection changes.each(function reNode(d, i, j) { changes[j][i] = rects[j][i]; }); updateThen(changes); this.done.resolve(changes); this.done = $.Deferred(); }; rebind.postChanges = function (rects, squares) { var rects = dataFrame.selectionBuffer(rects), squares = dataFrame.dataBuffer(squares), data = { method: "changes", rects: rects, squares: squares }; rebind.postMessage(data, [rects.buffer]); return data }; rebind.key = function (data) { this.done.resolve(data); this.done = $.Deferred(); }; rebind.done = $.Deferred(); //standard methods rebind.postMessage({ method: "key", data: keyDescriptor }); rebind.onmessage = function (e) { //invoke the method on the data this[e.data.method](e.data.data); }; return rebind; function selectionToBuff(selection) { return selection.map(function group(g) { return JSON.stringify(g.map(function node(d) { return d.__data__ })); }); } function selectionFromBuff(selectionJSON) { return selectionJSON.map(function (g) { return JSON.parse(g).map(function (d) { return d ? { __data__: d } : undefined }); }); } } elapsedTime.message(function (value) { var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap); return 'frame rate: ' + d3.format(" >7,.1f")(1/aveLap) }); elapsedTime.start(1000); d3.timer(function () { elapsedTime.mark(); if(elapsedTime.aveLap.history.length) hist(elapsedTime.aveLap.history); }) });